<?php

namespace s94\wechat;

/**
 * 微信公众号消息管理，包括模板消息
 */
class Message extends Base
{
    public $message = [];
    private $needEncrypt = false; //是否需要加密消息

    public function __construct($config)
    {
        $config['EncodingAESKey'] = $config['EncodingAESKey'] ?? '';
        $config['token'] = $config['token'] ?? '';
        parent::__construct($config);
    }

    /**xml编码
     * @param array $arr 编码的数组
     * @return string
     */
    private static function xml_encode($arr): string
    {
        $xml = "";
        foreach ($arr as $key=>$val){
            $key = is_numeric($key) ? 'item' : $key;
            $val = is_array($val) ? self::xml_encode($val) : (is_numeric($val) ? $val : "<![CDATA[{$val}]]>");
            $xml .= "<{$key}>{$val}</{$key}>";
        }
        return $xml;
    }

    /**xml解码
     * @param string $xml 解码的xml字符
     * @return array
     */
    private static function xml_decode($xml)
    {
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    }

    /**签名
     * @param $timestamp
     * @param $nonce
     * @param $msg_encrypt
     * @return string
     */
    private function sign($timestamp, $nonce, $msg_encrypt)
    {
        $arr = [$this->config('token'), $timestamp, $nonce, $msg_encrypt];
        sort($arr, SORT_STRING);
        return sha1( implode( $arr ) );
    }

    /**加密消息
     * @param string $text 消息文本
     * @return string
     * @throws \Exception
     */
    protected function encryptMsg($text)
    {
        self::assert(strlen($this->config('EncodingAESKey'))==43, '配置【EncodingAESKey】异常');
        $key = base64_decode($this->config('EncodingAESKey') . "=");
        $iv = substr($key, 0, 16);
        $text = self::randomStr() . pack("N", strlen($text)) . $text . $this->config('appid');
        //数据采用 PKCS#7 填充，把$text字符长度填充到密钥长度(32)的整数倍
        $pad_len = 32 - (strlen($text) % 32); //需要填补的字符长度
        $pad_chr = chr($pad_len); //填补的字符，根据“需要填补的字符长度”转换得到
        for ($i=0; $i<$pad_len; $i++){
            $text .= $pad_chr;
        }
        $encrypted = openssl_encrypt($text, 'AES-256-CBC', $key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv);
        return base64_encode($encrypted);
    }

    /**解密消息
     * @param string $encrypt 加密后的消息内容
     * @return array
     * @throws \Exception
     */
    protected function decryptMsg($encrypt)
    {
        self::assert(strlen($this->config('EncodingAESKey'))==43, '配置【EncodingAESKey】异常');
        $sign = $this->sign($_GET["timestamp"] ?? '', $_GET["nonce"] ?? '', $encrypt);
        self::assert(($_GET["msg_signature"] ?? '')==$sign, '消息签名错误');
        $key = base64_decode($this->config('EncodingAESKey') . "=");
        $iv = substr($key, 0, 16);
        $text = openssl_decrypt(base64_decode($encrypt), 'aes-256-cbc', $key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv);
        //去除补位字符
        $pad = ord(substr($text, -1));
        if ($pad < 1 || $pad > 32) {
            $pad = 0;
        }
        $text = substr($text, 0, -$pad);
        //$text的组成部分为，16个随机字符串 + 4字节(32位)长度用来表示消息长度的字符 + 消息体 + appid
        $msg_len = unpack("N", substr($text, 16, 4))[1];
        $msg = substr($text, 20, $msg_len);
//        $appid = substr($text, $xml_len + 20);
        return self::xml_decode($msg);
    }

    /**接收消息时，验证signature，尝试接入
     * @return false|mixed
     */
    public function checkSignature()
    {
        if (empty($_GET['signature']) || empty($_GET['timestamp']) || empty($_GET['nonce']) || empty($this->config('token'))) return false;
        $signature = $_GET["signature"];
        $timestamp = $_GET["timestamp"];
        $nonce = $_GET["nonce"];
        $token = $this->config('token');
        $sign = $this->sign($timestamp, $nonce, '');
        self::assert($signature==$sign, '消息错误');
        return $_GET['echostr'];
    }

    /**获取收到的消息内容，自动判断明文还是加密
     * @param mixed $secure 是否强制为安全模式，如果为true，收到的消息没有Encrypt字段的话会返回空数组
     * @return array
     * @throws \Exception
     */
    public function msg($secure=false)
    {
        if ($this->message) return $this->message;
        $body = file_get_contents('php://input');

        if ($body) $this->message = self::xml_decode($body);
        if (!empty($this->message['Encrypt'])){
            $this->needEncrypt = true;
            $this->message = $this->decryptMsg($this->message['Encrypt']);
        }elseif($secure){
            $this->message = [];
        }
        return $this->message;
    }


    private function newsArticles(array $data){
        if (!count($data)) return [];
        $data = isset($data[0]) && is_array($data[0]) ? $data : [$data];
        $res = [];
        foreach ($data as $row){
            $res[] = [
                'Title'=> $row['Title'] ?? '',
                'Description'=> $row['Description'] ?? '',
                'PicUrl'=> $row['PicUrl'] ?? '',
                'Url'=> $row['Url'] ?? '',
            ];
        }
        return $res;
    }
    /**被动回复用户消息
     * @param string $type 消息类型，包括：text,image,voice,video,music,news
     * @param mixed $data 消息数据，text为文本内容；image,voice,video为素材id；music为头图素材id；news为数组字段包含Title,Description,PicUrl,Url，多个消息为二维数组
     * @param array $extend 消息扩展数据，非必填的数据，具体参考文档，当$type为news时，此参数也可以按$data的格式传入消息数据，最终结果会合并$data和$extend
     * @return string
     */
    public function returnMsg(string $type, $data, array $extend=[]): string
    {
        $message = $this->msg();
        if (!$message || !in_array($type, ['text','image','voice','video','music','news'])) return '';
        $res = [
            'ToUserName' => $message['FromUserName'],
            'FromUserName' => $message['ToUserName'],
            'CreateTime' => time(),
            'MsgType' => $type,
        ];
        switch ($type){
            case 'text': $res['Content'] = $data;break;
            case 'image': $res['Image'] = ['MediaId'=>$data];break;
            case 'voice': $res['Voice'] = ['MediaId'=>$data];break;
            case 'video': {
                $res['Video'] = ['MediaId'=>$data];
                if (isset($extend['Title'])) $res['Video']['Title'] = $extend['Title'];
                if (isset($extend['Description'])) $res['Video']['Description'] = $extend['Description'];
            };break;
            case 'music': {
                $res['Music'] = ['ThumbMediaId'=>$data];
                self::assert(!empty($extend['MusicUrl']), '回复音乐消息，MusicUrl 参数不能为空');
                if (isset($extend['Title'])) $res['Music']['Title'] = $extend['Title'];
                if (isset($extend['Description'])) $res['Music']['Description'] = $extend['Description'];
                if (isset($extend['MusicUrl'])) $res['Music']['MusicUrl'] = $extend['MusicUrl'];
                if (isset($extend['HQMusicUrl'])) $res['Music']['HQMusicUrl'] = $extend['HQMusicUrl'];
            }break;
            case 'news': {
                //Title,Description,PicUrl,Url统一调整为二维数组里面
                $data = is_array($data) ? $this->newsArticles($data) : [];
                $extend = $this->newsArticles($extend);
                $data = array_merge($data, $extend);
                $res['ArticleCount'] = count($data);
                $res['Articles'] = $data;
            }break;
        }
        $xml = '<xml>'.self::xml_encode($res).'</xml>';
        if ($this->needEncrypt){
            $msg_encrypt = $this->encryptMsg($xml);
            $timestamp = time();
            $nonce = self::randomStr();
            $data = [
                'Encrypt'=> $msg_encrypt,
                'MsgSignature'=> $this->sign($timestamp, $nonce, $msg_encrypt),
                'TimeStamp'=> $timestamp,
                'Nonce'=> $nonce,
            ];
            $xml = '<xml>'.self::xml_encode($data).'</xml>';
        }
        return $xml;
    }

    //---------------------------------------模板消息------------------------------------------
    /**发送模板消息
     * @param string $openid openid
     * @param string $template_id 模板消息id
     * @param array $data 消息参数，格式：['first'=>'文本内容', 'keyword1'=>'以方括号包含的16进制颜色结尾表示此文本的颜色[#ff0000]',...]
     * @param string $url 跳转链接，跳转到网页表示网站url，跳转到小程序，表示小程序页面路径，支持带参数
     * @param string $mini_appid 小程序appid，不传表示跳转到网页
     * @return mixed
     * @throws SdkException
     */
    public function template($openid, $template_id, $data, $url=null, $mini_appid=null)
    {
        foreach ($data as &$v){
            $value = $v;
            $color = null;
            if (preg_match("/\[(\#[0-9a-f]{6})\]$/", $value, $ms)) {
                $color = $ms[1];
                $value = substr($value, 0, -9);
            }
            $v = ['value'=>$value];
            if ($color) $v['color'] = $color;
        }
        $post_data = [
            'touser'=>$openid,
            'template_id'=>$template_id,
            'data'=>$data
        ];
        if ($mini_appid){
            $post_data['miniprogram'] = ['appid'=>$mini_appid];
            if ($url) $post_data['miniprogram']['pagepath'] = $url;
        }elseif ($url){
            $post_data['url'] = $url;
        }
        $res = $this->apiSdk('cgi-bin/message/template/send', ['access_token'=>$this->accessToken()], json_encode($post_data));
        return $res;
    }

    /**模板消息的模板列表
     * @return mixed 格式：[['template_id'=>模板id,'title'=>标题,'primary_industry'=>一级行业,'deputy_industry'=>二级行业,'content'=>内容,'example'=>示例],...]
     * @throws SdkException
     */
    public function templateList()
    {
        $res = $this->apiSdk('cgi-bin/template/get_all_private_template', ['access_token'=>$this->accessToken()]);
        return $res['template_list'];
    }

    //--------------------------------------------订阅消息//TODO 待测试 --------------------------------

    public function subscribe($openid, $template_id, $title, $content, $scene, $url=null, $mini_appid=null)
    {
        $color = '#000000';
        if (preg_match("/\[(\#[0-9a-f]{6})\]$/", $content, $ms)) {
            $color = $ms[1];
            $content = substr($content, 0, -9);
        }
        $data = ['content'=> ['value'=>$content, 'color'=>$color]];
        $post_data = [
            'touser'=>$openid,
            'template_id'=>$template_id,
            'scene'=>$scene,
            'title'=>$title,
            'data'=>$data,
        ];
        if ($mini_appid){
            $post_data['miniprogram'] = ['appid'=>$mini_appid];
            if ($url) $post_data['miniprogram']['pagepath'] = $url;
        }elseif ($url){
            $post_data['url'] = $url;
        }
        $res = $this->apiSdk('cgi-bin/message/template/subscribe', ['access_token'=>$this->accessToken()], json_encode($post_data));
        return $res;
    }

    public function subscribeAsk(int $scene, string $template_id=null)
    {
        $reserved = 'subscribeAsk_'.$this->config('appid');

        if(empty($_GET['openid']) || empty($_GET['template_id']) || empty($_GET['reserved']) || $_GET['reserved']!=$reserved){
            self::assert($scene>=0 && $scene<=1000, "scene 场景值参数错误");
            self::assert($template_id, "template_id 消息模板ID不能为空");
            $http_type = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
            $redirect_url = $http_type.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
            $param = [
                'action' => 'get_confirm',
                'appid' => $this->config('appid'),
                'scene' => $scene,
                'template_id' => $template_id,
                'redirect_url' => $redirect_url,
                'reserved' => $reserved,
            ];
            $url = 'https://mp.weixin.qq.com/mp/subscribemsg?'.http_build_query($param).'#wechat_redirect';
            header('Location: '.$url);
            exit();
        }else{
            return $_GET;
        }
    }


}
